package net.sf.ivyide.idea;

import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.ModifiableModuleModel;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.roots.*;
import com.intellij.openapi.roots.libraries.Library;
import com.intellij.openapi.roots.libraries.LibraryTable;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
import net.sf.ivyide.*;

import java.io.File;
import java.util.*;

public class UpdateModule
        implements ProgressAwareRunnable {

    private static final Logger LOGGER = Logger.getInstance(UpdateModule.class.getName());

    private ModuleManager m_moduleManager;
    private Map<String, Library> m_usedProjectLibraries;
    private final ModifiableRootModel m_rootModel;
    private ResolvedModule m_resolvedModule;
    private SyncContext syncContext;

    public UpdateModule(ModuleManager settings, ResolvedModule resolvedModule, ModifiableRootModel rootModel, Map<String, Library> usedProjectLibraries, ModifiableModuleModel modulesModel, SyncContext syncContext) {
        m_rootModel = rootModel;
        m_resolvedModule = resolvedModule;
        m_moduleManager = settings;
        m_usedProjectLibraries = usedProjectLibraries;
        this.syncContext = syncContext;
    }

    private Library synchronizeProjectLib(final Dependency libraryId, ProgressIndicator progressIndicator) {
        String name = (new StringBuilder()).append(libraryId.getName()).append("-").append(libraryId.getRevision()).toString();
        return synchronizeProjectLib(name, new LibraryDescriptorProvider() {
            public LibraryDescriptor getDescriptor() {
                return m_moduleManager.getLibraryDescriptor(m_resolvedModule, libraryId);
            }
        }, progressIndicator);
    }

    private static interface LibraryDescriptorProvider {

        LibraryDescriptor getDescriptor();

    }

    private Library synchronizeProjectLib(final String libraryName, LibraryDescriptorProvider libraryDescriptorProvider, ProgressIndicator progressIndicator) {
        String name = libraryName;
        Library cachedLibrary = m_usedProjectLibraries.get(name);
        if (cachedLibrary == null) { //no yet synced (same artifact can be synced by different modules)
            LibraryDescriptor descriptor = libraryDescriptorProvider.getDescriptor();
            Library library = synchronizeLib(syncContext.getProjectLibraryTableModel(), name, descriptor, progressIndicator);
            m_usedProjectLibraries.put(name, library);
            return library;
        } else {
            return cachedLibrary;
        }

    }


    private Library synchronizeLib(final LibraryTable.ModifiableModel libraryTable, final String name, final LibraryDescriptor descriptor, ProgressIndicator progressIndicator) {
        progressIndicator.setText2("Synchronizing library " + name);
        boolean modified = false;
        Library libraryByName = libraryTable.getLibraryByName(name);
        if (libraryByName == null) {
            LOGGER.debug("New library " + name);
            modified = true;
            libraryByName = libraryTable.createLibrary(name);
        }
        Library.ModifiableModel libModifiableModel = syncContext.getLibraryModel(libraryByName);
        VirtualFile[] classFiles = UpdateModule.this.createVirtualFiles(descriptor.getClasses(), progressIndicator);
        VirtualFile[] sourceFiles = UpdateModule.this.createVirtualFiles(descriptor.getSources(), progressIndicator);
        modified = modified | UpdateModule.syncRoots(classFiles, libModifiableModel, OrderRootType.CLASSES);
        modified = modified | UpdateModule.syncRoots(sourceFiles, libModifiableModel, OrderRootType.SOURCES);
        LOGGER.debug("Library modified " + name + ": " + modified);
        return libraryByName;
    }

    private static boolean syncRoots(VirtualFile[] classes, Library.ModifiableModel libModifiableModel, OrderRootType rootType) {
        boolean modified = false;
        List<String> oldUrls = new LinkedList<String>(Arrays.asList(libModifiableModel.getUrls(rootType)));
        for (VirtualFile virtualFile : classes) {
            boolean alreadyAvailable = false;
            for (String oldUrl : oldUrls) {
                if (oldUrl.equals(virtualFile.getUrl())) {
                    alreadyAvailable = true;
                    oldUrls.remove(oldUrl);
                    break;
                }
            }
            if (!alreadyAvailable) {
                modified = true;
                libModifiableModel.addRoot(virtualFile, rootType);
            }
        }
        //Remove old stuff that is not available in the new stuff
        for (String oldUrl : oldUrls) {
            modified = true;
            UpdateModule.LOGGER.info("Removing " + oldUrl + " from " + libModifiableModel.getName());
            libModifiableModel.removeRoot(oldUrl, rootType);
        }
        return modified;
    }

    private ContentEntry getContentEntry(VirtualFile myContentRoot) {

        ContentEntry[] contentEntries = m_rootModel.getContentEntries();
        for (ContentEntry contentEntry : contentEntries) {
            if (myContentRoot.equals(contentEntry.getFile())) {
                return contentEntry;
            }
        }
        return m_rootModel.addContentEntry(myContentRoot);
    }

    private void addSource(final Collection<SourceFolder> unusedSourceFolders, final Collection<ExcludeFolder> unusedExcludes, final Set<ContentEntry> unusedEntries, final ContentRoot[] contentRoots, ProgressIndicator progressIndicator) {
        for (ContentRoot contentRoot : contentRoots) {
            VirtualFile virtualRootDir = LocalFileSystem.getInstance().findFileByIoFile(contentRoot.getDir());
            if (virtualRootDir == null) {
                UpdateModule.LOGGER.error("Unexisting root: " + virtualRootDir);
            } else {
                ContentEntry contentEntry = getContentEntry(virtualRootDir);
                unusedEntries.remove(contentEntry);
                File[] mainJavaDirs = contentRoot.getMainJavaDirs();
                for (File mainJavaDir : mainJavaDirs) {
                    syncSource(contentEntry, mainJavaDir, unusedSourceFolders, false);
                }
                File[] testJavaDirs = contentRoot.getTestJavaDirs();
                for (File testJavaDir : testJavaDirs) {
                    syncSource(contentEntry, testJavaDir, unusedSourceFolders, true);
                }
                File[] excludedDirs = contentRoot.getExcludedDirs();
                for (File excludedDir : excludedDirs) {
                    VirtualFile fileByIoFile = LocalFileSystem.getInstance().findFileByIoFile(excludedDir);
                    if (fileByIoFile != null) {
                        Iterator<ExcludeFolder> unusedIterator = unusedExcludes.iterator();
                        while (unusedIterator.hasNext()) {
                            ExcludeFolder excludeFolder = unusedIterator.next();
                            if (fileByIoFile.equals(excludeFolder.getFile())) {
                                unusedIterator.remove();
                            }
                        }
                        contentEntry.addExcludeFolder(fileByIoFile);
                    }
                }
            }
        }
    }

    private void syncSource(ContentEntry contentEntry, File mainJavaDir, Collection<SourceFolder> unusedSourceFolders, boolean test) {
        VirtualFile fileByIoFile = LocalFileSystem.getInstance().findFileByIoFile(mainJavaDir);
        if (fileByIoFile != null) {
            Iterator<SourceFolder> iterator = unusedSourceFolders.iterator();
            while (iterator.hasNext()) {
                SourceFolder sourceFolder = iterator.next();
                if (fileByIoFile.equals(sourceFolder.getFile())) {
                    if (!(sourceFolder.isTestSource() ^ test)) {
                        iterator.remove();
                        return;
                    }
                }

            }
            contentEntry.addSourceFolder(fileByIoFile, test);
        }
    }

    public void run(final ProgressIndicator progressIndicator) {
        try {
            LOGGER.debug("Updating " + m_resolvedModule.getModuleId());
            progressIndicator.setText2("");
            progressIndicator.setText("Synchronizing module " + m_resolvedModule.getModuleId());
            final List<String> dependantModules = new LinkedList<String>();
            final Map<ModuleId, File> modules = m_moduleManager.getModules();
            ModuleSourceDescriptor rootModuleDescriptor = m_moduleManager.getModuleSource(modules.get(m_resolvedModule.getModuleId()));
            File compilerOutputPath = rootModuleDescriptor.getCompilerOutputPath();
            CompilerModuleExtension compilerModule = m_rootModel.getModuleExtension(CompilerModuleExtension.class);
            if (compilerOutputPath != null) {
                String path = compilerOutputPath.getPath();
                String url = VirtualFileManager.constructUrl("file", path.replace(File.separatorChar, '/'));
                compilerModule.inheritCompilerOutputPath(false);
                compilerModule.setCompilerOutputPath(url);
            }
            File compilerOutputPathForTests = rootModuleDescriptor.getCompilerOutputPathForTests();
            if (compilerOutputPathForTests != null) {
                compilerModule.setCompilerOutputPathForTests(VirtualFileManager.constructUrl("file", compilerOutputPathForTests.getPath().replace(File.separatorChar, '/')));
            }
            compilerModule.setExcludeOutput(true);

            //clean (non syntetic) order entries
            for (OrderEntry orderEntry : m_rootModel.getOrderEntries()) {
                if (!orderEntry.isSynthetic())
                    m_rootModel.removeOrderEntry(orderEntry);
            }
            m_rootModel.inheritSdk();

            final Map<Library, DependencyScope> librariesUseByThisModule = new LinkedHashMap<Library, DependencyScope>();

            Map<ModuleId, DependencyType> internalModules = m_moduleManager.getDependantModules(m_resolvedModule);
            final Set<SourceFolder> unusedSourceFolders = new HashSet<SourceFolder>();
            final Set<ContentEntry> unusedContentEntries = new HashSet<ContentEntry>();
            final ContentEntry[] contentEntries = m_rootModel.getContentEntries();
            final Collection<ExcludeFolder> unusedExcludes = new LinkedList<ExcludeFolder>();
            ProgressUtilities.executeAsReadAction(new ProgressAwareCallable<Object>() {
                public Object call(ProgressIndicator indicator) throws Exception {
                    for (ContentEntry contentEntry : contentEntries) {
                        unusedContentEntries.add(contentEntry);
                        SourceFolder[] sourceFolders = contentEntry.getSourceFolders();
                        unusedSourceFolders.addAll(Arrays.asList(sourceFolders));
                        //getexcluded folders requires read access !!
                        unusedExcludes.addAll(Arrays.asList(contentEntry.getExcludeFolders()));
                    }
                    return null;
                }
            }, progressIndicator);
            addModuleContentRoots(librariesUseByThisModule, unusedSourceFolders, unusedExcludes, unusedContentEntries, rootModuleDescriptor, progressIndicator);

            final File[] extraRuntimeLibrary = rootModuleDescriptor.getExtraRuntimeLibary();
            if (extraRuntimeLibrary.length > 0) {
                synchronizeProjectLib(extraRuntimeLibrary[0].getPath(), new LibraryDescriptorProvider() {
                    public LibraryDescriptor getDescriptor() {
                        return new LibraryDescriptorImpl(extraRuntimeLibrary);
                    }
                }, progressIndicator);
            }
            addFilesAsLibrary(librariesUseByThisModule, progressIndicator, syncContext.getProjectLibraryTableModel(), DependencyScope.RUNTIME, extraRuntimeLibrary);
            Collection<Dependency> dependencies = m_resolvedModule.getDependencies();
            for (final Dependency dependency : dependencies) {
                DependencyType dependencyType = internalModules.get(dependency.getModuleId());
                UpdateModule.LOGGER.debug("DependencyType for " + dependency + " " + dependencyType);
                if (dependencyType == null || dependencyType.equals(DependencyType.LIBRARY)) {
                    Library library = synchronizeProjectLib(dependency, progressIndicator);
                    librariesUseByThisModule.put(library, dependency.getScope());
                } else {
                    String dependantModuleName = dependency.getModuleId().getName();

                    //m_rootModel.addInvalidModuleEntry(dependantModuleName); adding invalid module required read access.. Don't use it..
                    //this only works when committing models at the end...
                    Module dependantModule = syncContext.getModulesModel().findModuleByName(dependantModuleName);
//                    LOGGER.debug("Adding module dependency to module " + m_rootModel.getModule().getName() + ": " + dependantModule);
                    m_rootModel.addModuleOrderEntry(dependantModule).setScope(dependency.getScope());
                    dependantModules.add(dependantModuleName);
                    //ModuleSourceDescriptor moduleSourceDescriptor = m_moduleManager.getModuleSource(file);
                    //addModuleSource(librariesUseByThisModule, unusedSourceFolders, unusedContentEntries, moduleSourceDescriptor);
                }
            }
            progressIndicator.setText2("");
            LibraryTable.ModifiableModel moduleLibraryTableModel = m_rootModel.getModuleLibraryTable().getModifiableModel();
            synchronizeUsedLibraries(librariesUseByThisModule, moduleLibraryTableModel, m_rootModel, true);
            synchronizeUsedLibraries(librariesUseByThisModule, syncContext.getProjectLibraryTableModel(), m_rootModel, false);
            cleanUnusedLibraries(librariesUseByThisModule.keySet(), moduleLibraryTableModel);
            List<String> libraryNamesUsedByThisModule = new LinkedList<String>();
            for (Library library : librariesUseByThisModule.keySet()) {
                libraryNamesUsedByThisModule.add(library.getName());
            }
            LOGGER.debug("Used libraries: " + libraryNamesUsedByThisModule);
            cleanUnusedFolders(unusedSourceFolders, unusedExcludes, unusedContentEntries);
            //remove unused and duplicate module dependencies
            Set<String> orderEntries = new LinkedHashSet<String>();

            progressIndicator.setText2("");
            LOGGER.debug("Updating " + m_resolvedModule.getModuleId() + " done");
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private VirtualFile[] createVirtualFiles(File[] files, ProgressIndicator progressIndicator) {
        Collection<VirtualFile> libDirs = new LinkedList<VirtualFile>();
        for (final File file : files) {
            final VirtualFile virtualFile;
            if (file.isDirectory()) {
                virtualFile = LocalFileSystem.getInstance().findFileByIoFile(file);
            } else {//for jar files, special care is needed..
                //both jar and zip files should be handled like this..
                final String urlString = VirtualFileManager.constructUrl("jar", (new StringBuilder()).append(file.getAbsolutePath().replaceAll("\\\\", "/")).append("!/").toString());
                virtualFile = VirtualFileManager.getInstance().findFileByUrl(urlString);
            }
            if (virtualFile != null) {
                libDirs.add(virtualFile);
            } else {
                LOGGER.debug("Unable to find virtual file for " + file);
            }
        }
        return libDirs.toArray(new VirtualFile[libDirs.size()]);
    }

    private void checkDuplicates(Set<String> orderEntries, OrderEntry orderEntry) {
        if (orderEntries.contains(orderEntry.getPresentableName())) {
            //duplicate
            LOGGER.debug("Removing duplicate order entry: " + orderEntry.getPresentableName());
            m_rootModel.removeOrderEntry(orderEntry);
        }
        orderEntries.add(orderEntry.getPresentableName());
    }

    private void cleanUnusedFolders(Collection<SourceFolder> unusedSourceFolders, Collection<ExcludeFolder> unusedExcludes, Set<ContentEntry> unusedEntries) {
        ContentEntry[] contentEntries = m_rootModel.getContentEntries();
        for (ContentEntry contentEntry : contentEntries) {
            for (SourceFolder sourceFolder : unusedSourceFolders) {
                if (contentEntry.equals(sourceFolder.getContentEntry())) {
                    contentEntry.removeSourceFolder(sourceFolder);
                }
            }
            if (unusedEntries.contains(contentEntry)) {
                m_rootModel.removeContentEntry(contentEntry);
            }

            for (ExcludeFolder unusedExclude : unusedExcludes) {
                if (!unusedExclude.isSynthetic()) {
                    if (contentEntry.equals(unusedExclude.getContentEntry())) {
                        LOGGER.debug("Removing unused exclude " + unusedExclude.getUrl() + " " + unusedExclude.isSynthetic());
                        contentEntry.removeExcludeFolder(unusedExclude);
                    }
                }
            }
        }
    }

    private void synchronizeUsedLibraries(final Map<Library, DependencyScope> usedLibraries, final LibraryTable.ModifiableModel libraryTable, final ModifiableRootModel rootModel, final boolean export) {
        //for all entries in the model that are not used, remove order entry..
        Collection<Library> libraryIterator = new LinkedList<Library>(Arrays.asList(libraryTable.getLibraries()));
        for (Library library : libraryIterator) {
            DependencyScope scope = usedLibraries.get(library);
            if (scope == null) {
                LibraryOrderEntry libraryOrderEntry = rootModel.findLibraryOrderEntry(library);
                if (libraryOrderEntry != null) {
                    rootModel.removeOrderEntry(libraryOrderEntry);
                }
            } else {
                LibraryOrderEntry libraryOrderEntry = rootModel.findLibraryOrderEntry(library);
                if (libraryOrderEntry == null) {
                    //Altough the library is valid, we add an invalid library here...
                    //that works better on intellij 7. If we add the library directly,
                    //the returned order entry always gives back 'invalid' (for new libraries at least),
                    //and intellij get's messed up internally (red lines where there should not be..)
                    libraryOrderEntry = rootModel.addInvalidLibrary(library.getName(), library.getTable().getTableLevel());
                }
                libraryOrderEntry.setExported(export);
                libraryOrderEntry.setScope(scope);
            }
        }
    }

    static void cleanUnusedLibraries(Collection<Library> usedLibraries, LibraryTable.ModifiableModel libraryTable) {
        Collection<Library> libraries = new LinkedList<Library>(Arrays.asList(libraryTable.getLibraries()));
        List<String> usedLibraryNames = new LinkedList<String>();
        for (Library library : libraries) {
            if (!usedLibraries.contains(library)) {
                //remove
                LOGGER.debug("Removing unused library: " + library.getName() + " " + library);
                libraryTable.removeLibrary(library);
            } else if (usedLibraryNames.contains(library.getName())) {
                //removing duplicate..
                LOGGER.debug("Removing duplicate library: " + library.getName() + " " + library);
                libraryTable.removeLibrary(library);
            }
            usedLibraryNames.add(library.getName());
        }
    }

    private void addModuleContentRoots(Map<Library, DependencyScope> librariesUseByThisModule, Collection<SourceFolder> unusedSourceFolders, Collection<ExcludeFolder> unusedExcludes, Set<ContentEntry> unusedEntries, ModuleSourceDescriptor rootModuleDescriptor, ProgressIndicator progressIndicator) {
        ContentRoot[] contentRoots = rootModuleDescriptor.getContentRoots();
        addSource(unusedSourceFolders, unusedExcludes, unusedEntries, contentRoots, progressIndicator);

        LibraryTable.ModifiableModel moduleLibraryTable = m_rootModel.getModuleLibraryTable().getModifiableModel();

        for (ContentRoot contentRoot : contentRoots) {
            addFilesAsLibrary(librariesUseByThisModule, progressIndicator, moduleLibraryTable, DependencyScope.COMPILE, contentRoot.getMainResourceDirs());
            addFilesAsLibrary(librariesUseByThisModule, progressIndicator, moduleLibraryTable, DependencyScope.TEST, contentRoot.getTestResourceDirs());
        }
    }

    private void addFilesAsLibrary(Map<Library, DependencyScope> librariesUseByThisModule, ProgressIndicator progressIndicator, LibraryTable.ModifiableModel libraryTable, DependencyScope scope, File... files) {
        final List<File> libDirs = new LinkedList<File>();
        libDirs.addAll(Arrays.asList(files));
        if (libDirs.size() > 0) {
            Library library = synchronizeLib(libraryTable, libDirs.get(0).getPath(), new LibraryDescriptorImpl(libDirs.toArray(new File[libDirs.size()])), progressIndicator);
            librariesUseByThisModule.put(library, scope);
        }
    }

}
